<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use ReflectionMethod;

class Grove {

    /**
     * @var string
     */
    protected $defaultApplication;

    /**
     *
     * @var \mg\GroveContext
     */
    protected $context = null;

    /**
     *
     * @var \mg\Router
     */
    protected $router = null;

    /**
     * Create a new Grove web container with the given Router
     *
     * @param Router $router
     */
    public function __construct(Router $router) {
        // TODO possibly prevent Route logic issues by dropping injection of Route
        $this->context = get('mg\GroveContext');
        $this->router = $router;
    }

    /**
     * Set the default application to show
     *
     * @param string $defaultApplication
     */
    public function setDefaultApplication($defaultApplication) {
        $this->defaultApplication = $defaultApplication;
    }

    /**
     * Get the default application
     *
     * @return string
     */
    public function getDefaultApplication() {
        return $this->defaultApplication;
    }

    /**
     * Invoke a method on an instance of a Controller
     * TODO Perhaps use a interface for HttpRequest which mimics POST/query parameters
     *
     */
    public function invokeAction($instance, $action, array $parameters) {
        // Prevent the invocation of magic functions
        switch(mb_strtolower($action)) {
            case '__construct'	:
            case '__destruct'	:
            case '__call'		:
            case '__callstatic'	:
            case '__get'		:
            case '__set'		:
            case '__isset'		:
            case '__unset'		:
            case '__sleep'		:
            case '__wakeup'		:
            case '__tostring'	:
            case '__invoke'		:
            case '__set_state'	:
            case '__clone'		:
                throw new Exception('Unsafe controller action');
            default :
        }

        $executionParameters = array();

        try {
            $actionMethod = new ReflectionMethod($instance, $action);
            	
            $prototypeMethod = $actionMethod->getPrototype();

            if($prototypeMethod === null) {
                throw new Exception("Unsafe controller action");
            }
            	
            /* @var $declaringClass \ReflectionClass */
            $declaringClass = $prototypeMethod->getDeclaringClass();

            if(!($declaringClass->isInterface() && $declaringClass->isSubclassOf('\mg\Actions'))) {
                throw new Exception("Unsafe controller action");
            }

            // Foreach parameter in the actionMethod ..
            /* @var $parameter \ReflectionParameter */
            foreach($actionMethod->getParameters() as $parameter) {
                // .. try to install this parameter ..
                /* @var $class ReflectionClass */
                $class = $parameter->getClass();

                // Extract value from context first

                if($class !== null && $class->hasMethod('resolveInstance')) {
                    $contextMethod = $class->getMethod('resolveInstance');

                    // If resolveInstance exists, use resolve to figure out the parameter
                    if($contextMethod !== null) {
                        $executionParameters[] = $contextMethod->invoke(null, $parameter->getName());
                    } else {
                        $executionParameters[] = null;
                    }
                } elseif($class !== null && !$parameter->allowsNull()) {
                    throw new Exception("Unresolvable parameter : '{$parameter->getName()}'");
                    // If no class is set, use the action method parameters
                } /*elseif(isset($parameters[$parameter->getName()])) {
                $executionParameters[] = $parameters[$parameter->getName()];
                // .. or go for a default value, null if non existent
                }*/ else {
                if($parameter->isDefaultValueAvailable()) {
                    $executionParameters[] = $parameter->getDefaultValue();
                } else {
                    $executionParameters[] = null;
                }
                }
            }
        } catch(\Exception $e) {
            throw $e;
        }

        if($actionMethod === null) {
            return null;
        }

        return $actionMethod->invokeArgs($instance, $executionParameters);
    }

    protected function followRoute(HttpResponse $httpResponse, Route $route) {
        $context = $this->context;
        $defaultApplication = $this->defaultApplication;

        $grove = $this;

        while($route !== null) {
            $context->setRoute($route);

            // TODO remove $binder dependency
            $route = $context(function(ApplicationProvider $applicationProvider, Context $context, InjectionBinder $binder) use($grove, $route, $defaultApplication, $httpResponse) {
                $application	= $route->getApplication();
                $path			= $route->getPath();

                $module			= $route->getModule();

                $action			= $route->getAction();

                $actionParameters = $route->getActionParameters();

                if($application === null) {
                    $application = $defaultApplication;
                }

                $httpHeaders = array();
                $result = null;

                /* @var $applicationInstance Application */
                $applicationInstance = $applicationProvider->getApplication($application);

                if($applicationInstance === null) {
                    $result = new PageNotFoundResponse();
                } elseif($route->isConditional()) {
                    $profiles = $applicationProvider->getApplicationProfiles($application);

                    /* @var $profile Profile */
                    $profile = reset($profiles);

                    $targetedProfiles = array();

                    do {

                        array_unshift($targetedProfiles, $profile);

                        $result = $profile->beforeRoute($route);

                        if($result instanceof InternalHttpResponse) {
                            $httpHeaders = array_merge($httpHeaders, $result->getResponseHeaders());
                            $result = $result->getResponseBody();
                        }

                    } while($result === null && $profile->delegateRoute($route) && (($profile = next($profiles)) !== false));

                    unset($profiles);
                }

                if($result instanceof InternalHttpResponse) {
                    $httpHeaders = $result->getResponseHeaders();
                    $result = $result->getResponseBody();
                }

                // No blocking operation was performed, module
                if($result === null && $module !== null) {
                    $moduleProvider = $context(function(ModuleProvider $moduleProvider) { return $moduleProvider; });

                    $moduleInstance = $moduleProvider->getModule($application, $module);

                    // there is a module class
                    if($moduleInstance !== null && $result === null) {
                        try {
                            $result = $grove->invokeAction($moduleInstance, $action, $actionParameters);
                        } catch(\Exception $e) {
                            // Try all profiles (from bottom to top) for exception handling
                            $profile = reset($targetedProfiles);

                            do {
                                try {
                                    $result = $profile->handleException($route, $e);

                                    if($result instanceof InternalHttpResponse) {
                                        $httpHeaders = array_merge($httpHeaders, $result->getResponseHeaders());
                                        $result = $result->getResponseBody();
                                    }

                                    $e = null;
                                } catch(\Exception $e) {
                                }
                            }
                            while($result === null && $e !== null && (($profile = next($targetedProfiles)) !== false));

                            if($e !== null) {
                                $result = new ServerErrorResponse($e->getLine() . ' : ' . $e->getMessage());
                                Log :: s($e->getMessage());
                            }
                        }

                        if($result instanceof InternalHttpResponse) {
                            $httpHeaders = $httpHeaders === null ? $result->getResponseHeaders() : array_merge($httpHeaders, $result->getResponseHeaders());
                            $result = $result->getResponseBody();
                        }
                    }
                } elseif($result === null && $action !== null) {
                    try {
                        $result = $grove->invokeAction($applicationInstance, $action, $actionParameters);
                    } catch(\Exception $e) {
                        // Try all profiles (from bottom to top) for exception handling
                        $profile = reset($targetedProfiles);
                        do {
                            try {
                                $result = $profile->handleException($route, $e);

                                if($result instanceof InternalHttpResponse) {
                                    $httpHeaders = array_merge($httpHeaders, $result->getResponseHeaders());
                                    $result = $result->getResponseBody();
                                }

                                $e = null;
                            } catch(\Exception $e) {
                            }
                        }
                        while($result === null && $e !== null && (($profile = next($targetedProfiles)) !== false));

                        if($e !== null) {
                            $result = new ServerErrorResponse($e->getLine() . ' : ' . $e->getMessage());
                            Log :: s($e->getMessage());
                        }
                    }
                }

                // either module action or module was invoked
                if($result instanceof Route) {
                    return $result;
                } elseif($result instanceof InternalHttpResponse) {
                    $httpHeaders = array_merge($httpHeaders, $result->getResponseHeaders());
                    $result = $result->getResponseBody();
                }

                // Render the application by default
                if($result === null) {
                    $result = $applicationInstance;
                }

                // Write all bindings (TODO clean this up)
                $binder->unbindAll();

                // If headers are present, emit the headers
                if(count($httpHeaders) > 0) {
                    $httpResponse->emitResponseHeaders(function() use($httpHeaders) {
                        foreach($httpHeaders as $header) {
                            header($header);
                        }
                    });
                }

                $view = null;

                // Check whether a view can be provided
                if($result instanceof Viewable) {
                    $view = $result->getView();
                }

                // If no view is known, use the default view
                $view = $view === null ? in(function(ViewProvider $viewProvider) use($result) { return $viewProvider->createView($result); }) : $view;

                $httpResponse->emitResponseBody(function() use($view, $result) {
                    $view->write($result, new OutputBuffer());
                });
            });

        }
    }

    public function dispatch(HttpRequest $request, HttpResponse $response) {
        $route = $this->router->route($request);
        $this->followRoute($response, $route);
    }

}

/**
 * The GroveContext injects all applications and modules available in the application and module registry.
 *
 * @author Michiel Hakvoort
 *
 */
class GroveContext extends Context {

    private $route = null;

    public function __construct(Context $parentContext) {
        parent :: __construct($parentContext, function(Binder $binder) { });
    }

    public function setRoute(Route $route) {
        $this->route = $route;
    }

    protected function resolve($className, $name = null) {
        if($className === 'mg\route') {
            return $this->route;
        }

        return parent :: resolve($className, $name);
    }
}

/**
 * Utility thingy
 *
 */
final class mg {

    private final function __construct() {

    }

    public static function translate($literal) {

    }
}
